package com.zws.cucumber.util.colorfullog;

import com.datastax.oss.driver.api.core.data.UdtValue;
import com.zws.cucumber.util.colorfullog.table.ZwsPrefixRender;
import com.zws.cucumber.util.colorfullog.table.ZwsSequenceRender;
import com.zws.cucumber.util.colorfullog.table.ZwsTablePrinterConfig;
import org.apache.commons.lang3.tuple.Pair;
import org.slf4j.Logger;
import org.slf4j.event.Level;

import java.util.*;
import java.util.Map.Entry;
import java.util.function.Consumer;
import java.util.function.Supplier;
import java.util.stream.Collectors;

import static com.zws.cucumber.util.colorfullog.ZwsColorfulLogHelper.*;
import static com.zws.cucumber.util.colorfullog.table.ZwsTablePrinterConfig.newConfig;

/**
 * author: zws
 */
public class ZwsPrintTableHelper {

    private static final Logger logger = ZwsColorfulLogger.of(ZwsPrintTableHelper.class);
    private static final String NULL = "[null]";

    private static final char H_SYMBOL = '-', V_SYMBOL = '|', C_SYMBOL = '+', PADDING = ' ';

    private static String[] LAYOUT_SYMBOL_V_1 = new String[] { BG_GRAY_1, FG_YELLOW };
    private static String[] LAYOUT_SYMBOL_V_2 = new String[] { BG_GRAY_2, FG_YELLOW };
    private static String[] LAYOUT_CELL_1 = new String[] { BG_GRAY_1, FG_WHITE_1 };
    private static String[] LAYOUT_CELL_1_NULL = new String[] { BG_GRAY_1, FG_YELLOW };
    private static String[] LAYOUT_CELL_2 = new String[] { BG_GRAY_2, FG_WHITE_1 };
    private static String[] LAYOUT_CELL_2_NULL = new String[] { BG_GRAY_2, FG_YELLOW };

    private static String[] chooseCellLayout(int rowIdx, boolean hasMultipleLines, boolean isNull) {
        if (hasMultipleLines) {
            if (isNull) {
                return (rowIdx & 1) == 0 ? LAYOUT_CELL_1_NULL : LAYOUT_CELL_2_NULL;
            } else {
                return (rowIdx & 1) == 0 ? LAYOUT_CELL_1 : LAYOUT_CELL_2;
            }
        }
        if (isNull) {
            return LAYOUT_CELL_1_NULL;
        } else {
            return LAYOUT_CELL_1;
        }
    }

    private static String[] chooseSymbolVLayout(int rowIdx) {
        return (rowIdx & 1) == 0 ? LAYOUT_SYMBOL_V_1 : LAYOUT_SYMBOL_V_2;
    }

    private static class PrintTableContext {
        boolean hasMultipleLines = false;
        List<Integer> cellMaxWidths;
        List<List<Integer>> cellHeightsAllRows;
        List<List<List<Integer>>> lineIndexesAllRows;

        public int getCellMaxWidth(int columnIdx) {
            return cellMaxWidths.get(columnIdx);
        }

        public int getCellHeight(int rowIdx, int columnIdx) {
            return cellHeightsAllRows.get(rowIdx).get(columnIdx);
        }

        public int getMaxCellHeightInOneRow(int rowIdx) {
            return cellHeightsAllRows.get(rowIdx).stream().max(Comparator.comparingInt(Integer::intValue)).orElse(1);
        }

        public Pair<Integer, Integer> getLineIndex(int rowIdx, int columnIdx, int lineIdx) {
            List<Integer> lineIndexes = lineIndexesAllRows.get(rowIdx).get(columnIdx);
            if (lineIdx < lineIndexes.size()) {
                return Pair.of(lineIdx == 0 ? 0 : lineIndexes.get(lineIdx - 1) + 1, lineIndexes.get(lineIdx));
            } else {
                return Pair.of(0, 0);
            }
        }

    }

    public static <T> void printTable0(
            String tableName,
            Pair<List<String>, List<Map<String, T>>> headersAndRows) {

        if (logger.isDebugEnabled()) {
            printTable0(tableName, headersAndRows, false);
        }
    }

    public static <T> void printTable0(
            String tableName,
            Pair<List<String>, List<Map<String, T>>> headersAndRows,
            boolean printSeq) {

//        if (logger.isDebugEnabled()) {
//            ZwsSequenceRender seqRender = new ZwsSequenceRender(printSeq ? headersAndRows.getValue().size() : 0);
//            printTableString(makeTableString(tableName, headersAndRows, seqRender));
//        }

        ZwsTablePrinterConfig config = newConfig().withLogLevel(Level.DEBUG).withPrefixRender(new ZwsSequenceRender(printSeq ? headersAndRows.getValue().size() : 0));
        printTable0(tableName, headersAndRows, config);
    }

    public static <T> void printTable0(
            String tableName,
            Pair<List<String>, List<Map<String, T>>> headersAndRows,
            ZwsTablePrinterConfig config) {

        Supplier<Boolean> enableFunc = switch (config.logLevel()) {
            case ERROR -> logger::isErrorEnabled;
            case WARN -> logger::isWarnEnabled;
            case INFO -> logger::isInfoEnabled;
            case DEBUG -> logger::isDebugEnabled;
            case TRACE -> logger::isTraceEnabled;
        };
        if (enableFunc.get()) {
            printTableString(makeTableString(tableName, headersAndRows, config), config);
        }
    }

    public static void printTableString(String tableString) {
        printTableString(tableString, newConfig().withLogLevel(Level.DEBUG));
    }

    protected static void printTableString(String tableString, ZwsTablePrinterConfig config) {
        Consumer<String> func =
                switch (config.logLevel()) {
                    case ERROR -> logger::error;
                    case WARN -> logger::warn;
                    case INFO -> logger::info;
                    case DEBUG -> logger::debug;
                    case TRACE -> logger::trace;
                };
        func.accept("\n" + tableString);
    }

    public static <T> String makeTableString(
            String tableName,
            Pair<List<String>, List<Map<String, T>>> metadataAndRows,
            ZwsPrefixRender prefixRender) {

        return makeTableString(tableName, metadataAndRows, newConfig().withPrefixRender(prefixRender));
    }

    public static <T> String makeTableString(
            String tableName,
            Pair<List<String>, List<Map<String, T>>> metadataAndRows,
            ZwsTablePrinterConfig config) {

        ZwsPrefixRender prefixRender = config.prefixRender();
        List<String> colNames = metadataAndRows.getKey();
        List<Map<String, String>> rows = makePrintable(metadataAndRows.getValue());
        if (rows.isEmpty()) {
            return prefixRender.render() + render(tableName, FG_YELLOW, BOLD) + render(": no data", FG_YELLOW);
        }
        PrintTableContext context = initContext(colNames, rows);
        // separator
        String separator = makeRowSeparatorString(colNames, context.cellMaxWidths, prefixRender);

        StringBuilder buf = new StringBuilder();
        // top line
        buf.append(separator).append("\n");
        // header
        String headerRow = makeHeaderString(colNames, context.cellMaxWidths);
        buf.append(prefixRender.render()).append(render(headerRow, FG_YELLOW)).append("\n");
        // separator
        buf.append(separator).append("\n");
        // row
        for (int rowIdx = 0; rowIdx < rows.size(); rowIdx++) {

            Map<String, String> row = rows.get(rowIdx);
            int maxCellHeight = context.getMaxCellHeightInOneRow(rowIdx);
            for (int lineIdx = 0; lineIdx < maxCellHeight; lineIdx++) {
                buf.append(prefixRender.render(rowIdx));
                int colIdx = 0;
                for (String colName : colNames) {
                    String cell = row.get(colName);
                    Pair<Integer, Integer> lineIndex = context.getLineIndex(rowIdx, colIdx, lineIdx);
                    boolean isNull = cell == NULL;

                    int lenOfCurrentLine = lineIndex.getRight() - lineIndex.getLeft();
                    int lenOfPadding = context.cellMaxWidths.get(colIdx) - 1 - lenOfCurrentLine;
                    if (colIdx == 0) {
                        renderIn(buf, V_SYMBOL, FG_YELLOW);
                    } else if (context.hasMultipleLines) {
                        renderIn(buf, V_SYMBOL, chooseSymbolVLayout(rowIdx));
                    } else {
                        renderIn(buf, V_SYMBOL, LAYOUT_SYMBOL_V_1);
                    }
                    char[] p = new char[lenOfPadding];
                    Arrays.fill(p, PADDING);
                    renderIn(
                            buf,
                            new Object[]{ PADDING, new ZwsRenderableStringSegment(cell, lineIndex), p },
                            chooseCellLayout(rowIdx, context.hasMultipleLines, isNull));
                    colIdx++;
                }
                buf.append(render(V_SYMBOL, FG_YELLOW)).append("\n");
            }
        }
        // bottom line
        buf.append(separator);
        return prefixRender.render() + render(tableName, FG_YELLOW, BOLD) + "\n" + buf;
    }

    private static String makeHeaderString(List<String> colNames, List<Integer> cellWidths) {
        StringBuilder headerBuf = new StringBuilder();
        int idx = 0;
        for (String colName : colNames) {
            int lenOfCol = cellWidths.get(idx) - colName.length();
            headerBuf.append(V_SYMBOL);
            char[] lp = new char[lenOfCol / 2];
            char[] rp = new char[(lenOfCol + 1) / 2];
            Arrays.fill(lp, PADDING);
            Arrays.fill(rp, PADDING);
            headerBuf.append(lp).append(colName).append(rp);
            idx++;
        }
        headerBuf.append(V_SYMBOL);
        return headerBuf.toString();
    }

    private static String makeRowSeparatorString(List<String> colNames, List<Integer> cellWidths, ZwsPrefixRender prefixRender) {
        StringBuilder separatorBuf = new StringBuilder();
        for (int idx = 0; idx < colNames.size(); idx++) {
            int lenOfCol = cellWidths.get(idx);
            separatorBuf.append(C_SYMBOL);
            char[] p = new char[lenOfCol];
            Arrays.fill(p, H_SYMBOL);
            separatorBuf.append(p);
        }
        separatorBuf.append(C_SYMBOL);
        String separator = prefixRender.render() + render(separatorBuf.toString(), FG_YELLOW);
        return separator;
    }

    private static int getCellWidth(String cell) {
        if (cell == null) {
            return NULL.length();
        }

        if (!cell.contains("\n")) {
            return cell.length();
        }

        int currIdx = 0;
        int nextIdx = 0;
        int max = 0;
        do {
            nextIdx = cell.indexOf('\n', currIdx);
            if (nextIdx < 0) {
                max = Math.max(max, cell.length() - currIdx);
            } else {
                max = Math.max(max, nextIdx - currIdx);
                currIdx = nextIdx + 1;
            }
        } while(nextIdx >= 0);
        return max;
    }

    private static PrintTableContext initContext(List<String> columnNames, List<Map<String, String>> rows) {
        PrintTableContext context = new PrintTableContext();

        context.cellMaxWidths = getCellMaxWidths(columnNames, rows);

        List<List<Integer>> cellHeightAllRows = new ArrayList<>();
        List<List<List<Integer>>> lineIndexesAllRows = new ArrayList<>();

        for (Map<String, String> row : rows) {
            List<Integer> cellHeightsOneRow = new ArrayList<>();
            List<List<Integer>> lineIndexesOneRow = new ArrayList<>();
            for (String columnName : columnNames) {
                List<Integer> lineIndexes = new ArrayList<>();
                String cell = row.computeIfAbsent(columnName, k -> NULL);

                int currIdx = 0;
                int nextIdx = 0;
                int height = 1;
                do {
                    nextIdx = cell.indexOf('\n', currIdx);
                    if (nextIdx >= 0) {
                        currIdx = nextIdx + 1;
                        height++;
                        lineIndexes.add(nextIdx);
                    } else {
                        lineIndexes.add(cell.length());
                    }
                } while(nextIdx >= 0);
                cellHeightsOneRow.add(height);
                lineIndexesOneRow.add(lineIndexes);
            }
            cellHeightAllRows.add(cellHeightsOneRow);
            lineIndexesAllRows.add(lineIndexesOneRow);
        }
        context.hasMultipleLines = cellHeightAllRows.stream().flatMap(Collection::stream).anyMatch(h -> h > 1);
        context.cellHeightsAllRows = cellHeightAllRows;
        context.lineIndexesAllRows = lineIndexesAllRows;

        return context;
    }

    private static List<Integer> getCellMaxWidths(List<String> columnNames, List<Map<String, String>> rows) {
        List<Integer> widths = new ArrayList<>();
        for (String colName : columnNames) {
            Integer cellMaxWidth = rows.stream()
                    .map(r -> getCellWidth(r.get(colName)))
                    .max(Comparator.comparingInt(Integer::intValue))
                    .orElse(0);
            widths.add(Math.max(cellMaxWidth, colName.length()) + 2); // add 2 spaces
        }
        return widths;
    }

    private static <T> List<Map<String, String>> makePrintable(List<Map<String, T>> origin) {
        return origin.stream()
            .map(item -> {
                Map<String, String> newMap = new HashMap<>();
                for (Entry<String, T> en : item.entrySet()) {
                    newMap.put(en.getKey(), makePrintable(en.getValue()));
                }
                return newMap;
            })
            .collect(Collectors.toList());
    }

    private static <T> String makePrintable(T obj) {
        // null
        if (obj == null) {
            return null;
        }
        // number & string
        if (obj instanceof Number n) {
            return n.toString();
        }
        if (obj instanceof String s) {
            return s.replace("\r", "");
        }
        // array
        if (obj instanceof int[]) {
            return makeArrayPrintable((int[]) obj);
        }
        if (obj instanceof long[]) {
            return makeArrayPrintable((long[]) obj);
        }
        if (obj instanceof double[]) {
            return makeArrayPrintable((double[]) obj);
        }
        if (obj instanceof Object[]) {
            return makeArrayPrintable((Object[]) obj);
        }
        // collection
        if (obj instanceof Collection) {
            return ((Collection<?>) obj).stream()
                .map(ZwsPrintTableHelper::makePrintable)
                .collect(Collectors.joining(",", "[", "]"));
        }
        // map
        if (obj instanceof Map) {
            return ((Map<?, ?>) obj).entrySet().stream()
                    .map(en -> makePrintable(en.getKey()) + "=" + makePrintable(en.getValue()))
                    .collect(Collectors.joining(",", "{", "}"));
        }
        // udt
        if (obj instanceof UdtValue) {
            return ((UdtValue) obj).getFormattedContents();
        }
        return obj.toString().replace("\r", "");
    }

    private static <T> String makeArrayPrintable(T[] arr) {
        return Arrays.stream(arr)
            .map(ZwsPrintTableHelper::makePrintable)
            .collect(Collectors.joining(",", "[", "]"));
    }

    private static String makeArrayPrintable(int[] arr) {
        return Arrays.toString(arr);
    }

    private static String makeArrayPrintable(long[] arr) {
        return Arrays.toString(arr);
    }

    private static String makeArrayPrintable(double[] arr) {
        return Arrays.toString(arr);
    }

    public static void main(String[] args) {
        test1();
        test2();
    }

    private static void test1() {
        List<String> headers = List.of("Key", "Val");
        List<Map<String, String>> rows =
                List.of(
                        Map.of("Key", "errorCode", "Val", "invalid_request"),
                        Map.of("Key", "errorReason", "Val", "{\n  \"error_description\": \"Invalid Request.\"\n}"),
                        Map.of("Key", "errorReason2", "Val", "\n1\n23\n456\n78\n9\n"),
                        new HashMap<>() {{ put("Key", "key1"); put("Val", null); }}
                );
        long s = System.currentTimeMillis();
        printTable0("Test", Pair.of(headers, rows));
        System.out.println(System.currentTimeMillis() - s + "ms");
    }

    private static void test2() {
        List<String> headers = List.of("Key", "Val");
        List<Map<String, String>> rows =
                List.of(
                        Map.of("Key", "errorCode", "Val", "invalid_request"),
                        new HashMap<>() {{ put("Key", "key2"); put("Val", null); }},
                        Map.of("Key", "errorReason", "Val", "{\"error_description\": \"Invalid Request.\"}"),
                        Map.of("Key", "errorReason2", "Val", "1234567890"),
                        new HashMap<>() {{ put("Key", "key1"); put("Val", null); }}
                );
        long s = System.currentTimeMillis();
        printTable0("Test", Pair.of(headers, rows));
        System.out.println(System.currentTimeMillis() - s + "ms");
    }
}
